﻿namespace CloudNinja.Metering.Providers.AzureBlob
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;

    using CloudNinja.Metering.Common;
    using CloudNinja.Metering.Common.Configuration;
    using CloudNinja.Metering.Common.Logging;
    using CloudNinja.Metering.Data;

    using Microsoft.WindowsAzure.Storage;
    using Microsoft.WindowsAzure.Storage.Blob;

    using MeterData = CloudNinja.Metering.Data.Models.MeterData;

    [ProviderConfigElementType("CloudNinja.Metering.Providers.Configuration.BlobSize.BlobSizeProviderElement, CloudNinja.Metering.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")]
    public class BlobSizeProvider : MeteringProviderBase
    {
        private readonly Dictionary<string, TenantsStorageAccount> storageAccounts;

        public BlobSizeProvider(TimeSpan collectInterval, Dictionary<string, TenantsStorageAccount> storageAccounts, ILogger logger, IMeteringRepository repository, IMeteringContext meteringContext)
            : base(collectInterval, repository, logger, meteringContext)
        {
            Helper.Guard(collectInterval, "collectInterval");
            Helper.Guard(storageAccounts, "storageAccounts");
            Helper.Guard(logger, "logger");
            Helper.Guard(repository, "repository");

            this.storageAccounts = storageAccounts;
        }

        public override string Name
        {
            get { return "BlobSizeProvider"; }
        }

        public override void Initialize()
        {
            this.Logger.LogInformation("Initialized BlobSize Provider");
        }

        public override void Collect()
        {
            var results = new Dictionary<string, long>();
            var lockObject = new object();

            this.StartTiming();
            using (var sem = new SemaphoreSlim(1))
            {
                Parallel.ForEach(
                    this.storageAccounts.Keys,
                    () => new Dictionary<string, long>(),
                    (storageAccountName, loopstate, localDict) =>
                        {
                            CloudStorageAccount storageAccount;

                            if (
                                !CloudStorageAccount.TryParse(
                                    this.storageAccounts[storageAccountName].ConnectionString,
                                    out storageAccount))
                            {
                                throw new ArgumentException(string.Format("Connection string {0} cannot be parsed."));
                            }

                        var blobClient = new CloudBlobClient(
                            storageAccount.BlobEndpoint, storageAccount.Credentials);

                        var containers = new List<CloudBlobContainer>();

                        try
                        {
                            var retryPolicy = this.MeteringContext.AzureStorageRetryPolicy;

                            retryPolicy.ExecuteAction(
                                () => containers.AddRange(
                                    blobClient.ListContainers(
                                        this.storageAccounts[storageAccountName].TenantBaseContainerPath,
                                        ContainerListingDetails.All)));
                        }
                        catch (Exception exception)
                        {
                            this.Logger.LogException(exception);
                        }

                        foreach (var tenantContainer in
                            from container in containers
                            let tenantId = Helper.GetTenantId(container.Uri.Segments[container.Uri.Segments.Length - 1], this.storageAccounts[storageAccountName].TenantIdPattern)
                            where !string.IsNullOrEmpty(tenantId)
                            select new { Size = CalculateContainerSize(blobClient, container.Uri), Tenant = tenantId })
                        {
                            if (localDict.ContainsKey(tenantContainer.Tenant))
                            {
                                localDict[tenantContainer.Tenant] += tenantContainer.Size;
                            }
                            else
                            {
                                localDict.Add(tenantContainer.Tenant, tenantContainer.Size);
                            }
                        }

                        return localDict;
                    },
                    finalResult =>
                    {
                        lock (lockObject)
                        {
                            foreach (var l in finalResult)
                            {
                                results.Add(l.Key, l.Value);
                            }
                        }
                    });
            }

            this.MeteringRepository.Write(results.Select(x => new MeterData
                {
                    MeterTypeId = "stg",
                    TenantId = x.Key,
                    Value = x.Value,
                    TimePeriod = DateTime.UtcNow,
                    Count = 0
                }));

            this.StopTiming();
        }

        public override void Stop()
        {
            this.Logger.LogInformation("Stopping BLobSizeProvider.");
        }

        private long CalculateContainerSize(CloudBlobClient blobStore, Uri containerUri)
        {
            var blobContainer = blobStore.GetContainerReference(containerUri.AbsoluteUri);

            long totalSize = 0;

            var blobs = new List<IListBlobItem>();

            try
            {
                blobs.AddRange(blobContainer.ListBlobs(null, true, BlobListingDetails.Metadata | BlobListingDetails.Snapshots).ToList());
            }
            catch (Exception exception)
            {
                this.Logger.LogException(exception);
            }


            foreach (var blob in blobs.Select(blobItem => blobStore.GetBlobReferenceFromServer(blobItem.Uri)))
            {
                try
                {
                    blob.FetchAttributes();
                }
                catch (Exception exception)
                {
                    this.Logger.LogException(exception);
                }

                totalSize += blob.Properties.Length;
            }

            // The blob length property is the size in bytes. the whole metering framework uses kilobytes
            // convert to kilobytes, and round
            totalSize = Convert.ToInt64(Math.Round(totalSize / 1024f, 0));
            return totalSize;
        }
    }
}